var esprima = require('esprima');
var escodegen = require('escodegen');
var estraverse = require('estraverse');
var escope = require('escope');
//var esmangle = require('esmangle');

Syntax = estraverse.Syntax;

var esutils = require('esutils');
        
var rf=require("fs");
var beautify = require('js-beautify');

String.prototype.times = function(n) { 
    return Array.prototype.join.call({length:n+1}, this); 
}; 

var sourceFile = "formalmain.js"; //check if it's a one line
var baseName = sourceFile.split('.')[0];
var targetFile = baseName+ "target.js";
var ouputFile = baseName+"esp.js";
var tempCmSource =  baseName+"tempcmsource.js";

var program=rf.readFileSync(sourceFile,"utf-8");

function print_source(node){
    //console.log(source.slice(node.loc.start.column,node.loc.end.column ));
    var code=escodegen.generate(node);
    console.log(code);
}

function debug_node_source(node,ouputFile){
    var code=escodegen.generate(node);
    rf.writeFileSync(ouputFile,beautify(code , { indent_size: 2, space_in_empty_paren: true }));
}

function check_object_prototype_node(node){
    return node.type == 'CallExpression' && node.callee.type == 'MemberExpression'
                    && node.callee.object.type == 'MemberExpression'  && node.callee.object.object.type == 'MemberExpression'
                    && node.callee.object.object.object.name == 'Object'
                    && node.callee.object.object.property.name == 'prototype';
}

function mark_unsed_static_var(scope){
    console.log("start mark_unsed_static_var");
    
    for(var i=0;i<scope.variables.length; ++i){
        var v = scope.variables[i];
        v.isUnusedVar = false;
        if(v.name=='arguments'){
            continue;
        }
        //console.log("enter:"+v.name+",length:"+v.references.length );
        
        if(v.references.length == 1 && v.references[0].flag != 1){
            //ref = 1 and only write.
            v.isUnusedVar = true;
            console.log("isUnusedVar:"+v.name);
        }
    }
    
    for(var i=0;i<scope.childScopes.length; ++i){
        var childScope = scope.childScopes[i];
        mark_unsed_static_var(childScope);
    }
    
}

function mark_static_var_in_scope(scope){
    if(scope==null){
        console.log("why null");
        return;
    }
    
    console.log("start mark_static_var_in_scope");
    
    for(var i=0;i<scope.variables.length; ++i){
        var v = scope.variables[i];
        var write_count = 0;
        var write_node = null;
        //console.log("enter:"+v.name);
        v.isStaticVar = false; //add a property by me
        
        if(v.name=='arguments'){
            continue;
        }
        
        
        for(var j=0;j<v.references.length;++j){
            var ref = v.references[j];
            if(ref.flag > 1){
                ++write_count;
                if(ref.flag == 2){
                    write_node = ref.writeExpr;
                }
            }
        }
        
        if(write_count == 1 && write_node && write_node.type ==  Syntax.Literal){
            //判断静态变量依据，首先只写入一次，然后写入的是常量，其他都是应用
            v.valNode = write_node; //keep this node to use
            v.isStaticVar = true
            console.log("static val:"+v.name);
        }
        
        v.write_count = write_count;//keep write count for future use
    }
    
    for(var i=0;i<scope.childScopes.length; ++i){
        var childScope = scope.childScopes[i];
        mark_static_var_in_scope(childScope);
    }
}

function found_var_in_scope(varName,scope){
    
    for(var i=0;i<scope.variables.length; ++i){
        var v = scope.variables[i];
        if(v.name == varName){
            return v;
        }
    }
    return null; 
}

function found_static_def(node,scope){
    var v = found_var_in_scope(node.name,scope);
    if(v == null || v.isStaticVar == false){
        return null;
    }

    for(var j=0;j<v.references.length;++j){
            var ref = v.references[j];

            if(ref.identifier == node){
                if(ref.flag != 1){
                    //not read
                    return null;
                }
                return v;
            }
    }
    
    return null
}

function genNodeByEval(cmNode,ctx){
    var cm_callee = cmNode.callee.object.name + '.' + cmNode.callee.property.name;
    if(ctx.key2realval[cm_callee] == 'function'){
    
    }
    if(ctx.binaryOperator2name[cm_callee]){
              //get mid seperate line
              var binoperator = ctx.binaryOperator2name[cm_callee];
              var arg1 = cmNode.arguments[0];
              var arg2 = cmNode.arguments[1];
              
              return {

                    type: Syntax.BinaryExpression,
                    operator: binoperator,
                    left:arg1,
                    right:arg2
                
              };
              /*
              var arg1_loc = node.body[0].expression.arguments[0].loc;
              var arg2_loc = node.body[0].expression.arguments[1].loc;
              
              var arg1 = safeParam(src.slice(arg1_loc.start.column,arg1_loc.end.column));
              var arg2 = safeParam(src.slice(arg2_loc.start.column,arg2_loc.end.column));
              */
    }
    
    var code=escodegen.generate(cmNode);
    try{
        var result = eval(code);
        return {
                type: Syntax.Literal,
                value: result
                    };
    }catch(err){
        //console.log("eval fail:"+code);
        return cmNode;
    }
}

class ForSwitchNodes{
    
    constructor(varNode,expNode,varNode2,forNode){
        this.varNode = varNode;
        this.expNode = expNode;
        this.varNode2 = varNode2;
        this.forNode = forNode;
        
        this.fakeVars = new Array();
        this.fakeForVars = new Array();
        this.switchVarName = null;
        this.switchCase2Node = new Array();

    }
    
    getSwitchInfo(){
        var switchNode = this.forNode.body;
        this.switchVarName = switchNode.discriminant.name;
        console.log("switchVarName:"+ this.switchVarName );
        
        for(var i=0;i<switchNode.cases.length;++i){
            var caseNode = switchNode.cases[i];
            var caseCode = escodegen.generate(caseNode);
            
            console.log("getSwitchInfo:"+ caseCode );
            this.switchCase2Node[caseCode] = caseNode.consequent;
        }
    }
    
    getFakeVars(){
        var exps = this.expNode.expression.expressions;
        
        for(var i=0;i<exps.length;++i){
            var assignExp = exps[i];
            //console.log("getFakeVars:"+this.source.slice( assignExp.loc.start.column,assignExp.loc.end.column));
            if(assignExp.type != "AssignmentExpression")
                continue
            
            var varName = assignExp.left.name;
            console.log("getFakeVars:"+ varName + ":"+ escodegen.generate(assignExp.right));
            this.fakeVars[varName] = escodegen.generate(assignExp.right);
        }
    }
    
    getVarDeclares(){
        var declares = this.varNode2.declarations;
        for(var i=0;i<declares.length;++i){
            var decloreNode = declares[i];
            console.log("getVarDeclares:"+ decloreNode.id.name + ":"+escodegen.generate(decloreNode.init) );

            if(decloreNode.init)
                this.fakeForVars[decloreNode.id.name] = escodegen.generate(decloreNode.init);//this.source.slice(decloreNode.init.loc.start.column,decloreNode.init.loc.end.column);
            else
                this.fakeForVars[decloreNode.id.name] = null;
        }
    }
    
    getRealSource(){
        var varVal = this.fakeForVars[this.switchVarName];
        console.log("getRealSource:"+varVal);

        var souceNodes = this.switchCase2Node[varVal];
        //console.log(souceNodes);
        if(souceNodes==null){
            for(var name in this.switchCase2Node){
                return  this.switchCase2Node[name];
            }
        }

        return souceNodes;
    }
    
    
    getCleanNode(){
        this.getSwitchInfo();
        this.getFakeVars();
        this.getVarDeclares();
        var codeNodes = this.getRealSource();//胆子大点可以留一个节点。
        var resNode = null;
        
        console.log(codeNodes);
        if(codeNodes[0].type == "ExpressionStatement" && codeNodes[0].expression.type == "SequenceExpression"){
            resNode = codeNodes[0].expression.expressions[0];
        }else{
            resNode =codeNodes[0];
        }
        //console.log("codeNode:"+escodegen.generate(codeNodes) );
        return resNode;
    }
}

class SourceRevParser{
    
    constructor(source){
        this.source = source;
        this.cmSource = "";
        this.targetSource = "";
        this.targetAst = null;
        this.scopeManager = null;
        this.cmName = "";
        this.cmCalls = new Array();
        
        this.key2realval = new Array();
        this.binaryOperator2name = new Array();
    }
    
    analysis(){
        var ast = esprima.parseScript(this.source,{ loc: true });
        //mylog(ast.body[0].declarations[0].id);
        

        this.cmName = ast.body[1].declarations[0].id.name;
        this.cmNameRegName = this.cmName.substr(0,this.cmName.length-1);
        console.log("found cm name:"+this.cmName);
        
        var targetNode = ast.body[2];
         console.log("targetNode:"+targetNode.expression.type); 
        
        if(targetNode.expression.type !="UnaryExpression" && targetNode.expression.type !="CallExpression"&& targetNode.expression.type !="AssignmentExpression" ){
            mylog("code analysis wrong---"+targetNode.expression.type); 
            exit();
        }
        
        this.targetSource  = this.source.slice( targetNode.loc.start.column,targetNode.loc.end.column);
        this.cmSource =  this.source.slice( 0,targetNode.loc.start.column);
        //rf.writeFileSync(ouputFile,beautify(this.cmSource, { indent_size: 2, space_in_empty_paren: true }));//debug found
        rf.writeFileSync(tempCmSource,this.cmSource);
        rf.writeFileSync(targetFile,beautify(this.targetSource, { indent_size: 2, space_in_empty_paren: true }));//debug found
        
        this.targetAst = esprima.parseScript(this.targetSource,{ loc: true }) //targetNode;
    }
    
    collect_cm_defs(){
        //collect cm function define from cm source
        var cmObj = eval(this.cmName);

        for(var key in cmObj){
            var val = cmObj[key];
            var new_key = this.cmName + "." + key;
            //console.log("cm any: "+key+":"+val);
            //mylog(key+":"+val);
            
            if(typeof val == "function"){
                this.key2realval[new_key] = "function";
                var ast = esprima.parseScript( "("+val+")",{ loc: true });
                
                if(ast.body[0].expression.body.body[0].argument.operator )
                    this.binaryOperator2name[new_key] = ast.body[0].expression.body.body[0].argument.operator;
                
                
                //console.log("cm any: "+new_key + "["+ ast.body[0].expression.body.body[0].argument.operator );
                
            }else{
                this.key2realval[new_key] = val;
            }
        }
        
        for(var key in this.key2realval){
            //console.log('[cm def]' + key + ":" +  this.key2realval[key]);
        }
    }
    
    replace_cm_vals(){
        //1. replace all right cm val such as: v = _cw$.L,
        //2. replace all callee cm val xxx( _cw$.L)
        var self = this;
        estraverse.replace(this.targetAst, {
            enter: function (node, parent) {
                if (node.type == 'MemberExpression' && node.object.name == self.cmName && node.property.type == "Identifier"){
                    //1.首先必须是表达式 2.表达式名字必须是CM 3.表达式属性 不能是方法调用必须是单变量
                    if(parent && parent.type=='CallExpression' && parent.callee == node){
                        //it's a call expression, do nothing
                        return node;
                    }
                    /*
                    print_source(node,self.source);
                    print_source(parent,self.source);
                    console.log("----");
                    */
                    var cm_code = escodegen.generate(node);//self.source.slice(node.loc.start.column,node.loc.end.column );
                    var real_val = eval(cm_code);
                    return {
                        type: Syntax.Literal,
                        value: real_val
                    };
                }
                    
            },
            leave: function (node, parent) {
                
            }
        });
        //debug_node_source(this.targetAst,"replace_cm_vals.js");
    }
    
    replace_cm_calls(){
        var self = this;
        estraverse.replace(this.targetAst, {
            enter: function (node, parent) {
                /*
                if (node.type == 'CallExpression' && node.callee.type == 'MemberExpression' &&  node.callee.object.name == self.cmName){
                    //print_source(node);
                    return genNodeByEval(node,self);
                }
                */
                    
            },
            
            leave: function (node, parent) {
                
                if (node.type == 'CallExpression' && node.callee.type == 'MemberExpression' &&  node.callee.object.name == self.cmName){
                    //print_source(node);
                    return genNodeByEval(node,self);
                }
                
            }
            
        });
        debug_node_source(this.targetAst,"replace_cm_calls.js");
    }
    
    replace_brackets_with_dot(){
        var self = this;
        estraverse.replace(this.targetAst, {
            enter: function (node, parent) {
                /*
                if (node.type == 'CallExpression' && node.callee.type == 'MemberExpression' &&  node.callee.object.name == self.cmName){
                    //print_source(node);
                    return genNodeByEval(node,self);
                }
                */
                    
            },
            
            leave: function (node, parent) {
                
                if (node.type == 'MemberExpression' && node.computed == true 
                    && node.property.type == Syntax.Literal && typeof(node.property.value) == 'string'){
                    //print_source(node);
                    node.computed=false;
                    node.property = { type:Syntax.Identifier, name:node.property.value};
                    return node;
                }
                
            }
            
        });
        debug_node_source(this.targetAst,"replace_brackets_with_dot.js");
    }
    
    replace_obj_defineProperty(){
        
    }
    
    replace_obj_hasOwnProperty(){
        
        var self = this;
        estraverse.replace(this.targetAst, {
            enter: function (node, parent) {
                /*
                if (node.type == 'CallExpression' && node.callee.type == 'MemberExpression' &&  node.callee.object.name == self.cmName){
                    //print_source(node);
                    return genNodeByEval(node,self);
                }
                */
                    
            },
            
            leave: function (node, parent) {
                
                if (check_object_prototype_node(node)
                    && node.callee.object.property.name=='hasOwnProperty' && node.callee.property.name=='call'){
                    print_source(node);
                    return node;
                }
                
            }
            
        });
        debug_node_source(this.targetAst,"replace_brackets_with_dot.js");
    }
    
    remove_for_switch(){
        var self = this;
        estraverse.replace(this.targetAst, {
            enter: function (node, parent) {
                /*
                if (node.type == 'CallExpression' && node.callee.type == 'MemberExpression' &&  node.callee.object.name == self.cmName){
                    //print_source(node);
                    return genNodeByEval(node,self);
                }
                */
                    
            },
            
            leave: function (node, parent) {
                var forNodeIdx = 0;
                
                if(node.type==="BlockStatement" && node.body.length>=3){
                    for(var i=0;i<node.body.length-2;i++){
                        
                        var varNode = node.body[i];
                        var expNode = node.body[i+1];
                        var node3 = node.body[i+2];
                        
                        if(node3.type=="VariableDeclaration" && (i+3 < node.body.length)){
                            var varNode2 = node.body[i+2];
                            var forNode = node.body[i+3];
                            forNodeIdx = i+3;
                        }else if(node3.type=="ForStatement"){
                            var varNode2 = node3.init;
                            var forNode = node3;
                            forNodeIdx = i+2;
                        }else{
                            //no need test
                            continue
                        }
                        
                        //print_source(forNode);
                        //console.log('------start--------');
                            
                        if(varNode.type =="VariableDeclaration" && 
                            expNode.type =="ExpressionStatement" &&
                            varNode2.type =="VariableDeclaration" &&
                            forNode.type =="ForStatement" &&  forNode.body.type =="SwitchStatement" ){

                            var fwn = new ForSwitchNodes(varNode,expNode,varNode2,forNode);
                            var cleanNode = fwn.getCleanNode();
                            
                            node.body[forNodeIdx] = cleanNode;
                        }
                    }
                }
            }
        });
        debug_node_source(this.targetAst,"remove_for_switch.js");
    }
    
    replace_static_vars(){
        var self = this;
        self.scopeManager = escope.analyze(this.targetAst);
        //self.scopeManager.attach();
        var scope = self.scopeManager.globalScope;
        
        
        mark_static_var_in_scope(scope);

        //scope through 是指定义在 SCOPE范围外的变量的应用?
        //scope variables 是scope内部定义的变量
        //1. delete var with references.length == 1 
        //2. found all writeExpr times = 1 and then enter them
        estraverse.replace(this.targetAst, {
            
            enter: function (node, parent) {
                scope = self.scopeManager.acquire(node) || scope; //not all node has socpe
                
                //scope其实把引用节点都找出来了
                
                if (node.type == 'Identifier' ){
                    var vardef = found_static_def(node,scope);
                    if(vardef){
                        return vardef.valNode;
                    }
                }
            },
            
            leave: function (node, parent) {
               scope = self.scopeManager.release(node) || scope;
            }
            
        });
        
        self.scopeManager.release(this.targetAst);
        debug_node_source(this.targetAst,"replace_static_vars.js");

    }
    
    remove_unused_static_var(){
        var self = this;
        self.scopeManager = escope.analyze(this.targetAst);
        //self.scopeManager.attach();
        var scope = self.scopeManager.globalScope;
        mark_unsed_static_var(scope);
        
        estraverse.replace(this.targetAst, {
            
            enter: function (node, parent) {
                scope = self.scopeManager.acquire(node) || scope; //not all node has socpe
            },
            
            leave: function (node, parent) {
               if(node.type=='VariableDeclaration'){
                    var remain_declarations = new Array();
                    
                    for(var i=0;i<node.declarations.length;++i){
                        var dec = node.declarations[i];
                        var v = found_var_in_scope(dec.id.name,scope);
                        if(v && v.isUnusedVar){
                            //throw unused var
                            continue;
                        }else{
                            remain_declarations.push(dec);
                        }
                    }
                    
                    if(remain_declarations.length==0){
                        //remove declare node
                        return this.remove();
                    }else{
                        node.declarations = remain_declarations;
                    }
                    
               }else if(node.type == 'AssignmentExpression'){
                    var v = found_var_in_scope(node.left.name,scope);
                    if(v && v.isUnusedVar){
                        return this.remove();
                    }
               }
               
               scope = self.scopeManager.release(node) || scope;
            }
            
        });
        
        self.scopeManager.release(this.targetAst);
        debug_node_source(this.targetAst,"remove_unused_static_var.js");
    }
    
    remove_empty_statement(){
        
        estraverse.replace(this.targetAst, {
            enter: function (node, parent) {
                
            },
            
            leave: function (node, parent) {
                if(node.type=='EmptyStatement'){
                    return this.remove();
                }
            }
            
        });
        debug_node_source(this.targetAst,"remove_empty_statement.js");
    }
    
    replace_sequence_expression(){
        
        estraverse.replace(this.targetAst, {
            enter: function (node, parent) {
                
            },
            
            leave: function (node, parent) {
                if(node.type=='ExpressionStatement' && node.expression.type=='SequenceExpression'){
                    var newBody = new Array();
                    
                    if(parent.type=='SwitchCase'){
                        //replace SequenceExpression to block statement
                        for(var j=0;j<node.expression.expressions.length;++j){
                                    newBody.push({ type:'ExpressionStatement',
                                                    expression:node.expression.expressions[j]});
                         }
                         parent.consequent[0] = {
                            type:'BlockStatement',
                            body:newBody
                         };
                    }else{
                        /*
                        if(parent.body==null){
                            console.log("nullparent:");
                            console.log(parent);
                        }
                        */
                        for(var i=0;i < parent.body.length;++i){
                            var n = parent.body[i];
                            if(n==node){
                                //add it's inner instead of itself
                                for(var j=0;j<node.expression.expressions.length;++j){
                                    newBody.push({ type:'ExpressionStatement',
                                                    expression:node.expression.expressions[j]});
                                }
                            }else{
                                newBody.push(n);
                            }
                            
                           
                        }
                        parent.body = newBody;
                    }
                }
            }
        });
        debug_node_source(this.targetAst,"replace_sequence_expression.js");
    }
}


parser = new SourceRevParser(program);
parser.analysis();
var cm_source=rf.readFileSync(tempCmSource,"utf-8");
eval(cm_source);

parser.collect_cm_defs();
parser.replace_cm_vals();
parser.replace_cm_calls();
parser.replace_brackets_with_dot();
//parser.replace_obj_hasOwnProperty();
parser.remove_for_switch();
parser.replace_static_vars();
parser.replace_cm_calls();
parser.remove_unused_static_var();
parser.remove_empty_statement();
parser.replace_sequence_expression();

